/*! RowGroup 1.1.2
 * ©2017-2020 SpryMedia Ltd - datatables.net/license
 */

/**
 * @summary     RowGroup
 * @description RowGrouping for DataTables
 * @version     1.1.2
 * @file        dataTables.rowGroup.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     datatables.net
 * @copyright   Copyright 2017-2020 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net
 */

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'datatables.net'], function ($) {
            return factory($, window, document);
        });
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = function (root, $) {
            if (!root) {
                root = window;
            }

            if (!$ || !$.fn.dataTable) {
                $ = require('datatables.net')(root, $).$;
            }

            return factory($, root, root.document);
        };
    } else {
        // Browser
        factory(jQuery, window, document);
    }
}(function ($, window, document, undefined) {
    'use strict';
    var DataTable = $.fn.dataTable;


    var RowGroup = function (dt, opts) {
        // Sanity check that we are using DataTables 1.10 or newer
        if (!DataTable.versionCheck || !DataTable.versionCheck('1.10.8')) {
            throw 'RowGroup requires DataTables 1.10.8 or newer';
        }

        // User and defaults configuration object
        this.c = $.extend(true, {},
            DataTable.defaults.rowGroup,
            RowGroup.defaults,
            opts
        );

        // Internal settings
        this.s = {
            dt: new DataTable.Api(dt)
        };

        // DOM items
        this.dom = {};

        // Check if row grouping has already been initialised on this table
        var settings = this.s.dt.settings()[0];
        var existing = settings.rowGroup;
        if (existing) {
            return existing;
        }

        settings.rowGroup = this;
        this._constructor();
    };


    $.extend(RowGroup.prototype, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * API methods for DataTables API interface
         */

        /**
         * Get/set the grouping data source - need to call draw after this is
         * executed as a setter
         * @returns string~RowGroup
         */
        dataSrc: function (val) {
            if (val === undefined) {
                return this.c.dataSrc;
            }

            var dt = this.s.dt;

            this.c.dataSrc = val;

            $(dt.table().node()).triggerHandler('rowgroup-datasrc.dt', [dt, val]);

            return this;
        },

        /**
         * Disable - need to call draw after this is executed
         * @returns RowGroup
         */
        disable: function () {
            this.c.enable = false;
            return this;
        },

        /**
         * Enable - need to call draw after this is executed
         * @returns RowGroup
         */
        enable: function (flag) {
            if (flag === false) {
                return this.disable();
            }

            this.c.enable = true;
            return this;
        },

        /**
         * Get enabled flag
         * @returns boolean
         */
        enabled: function () {
            return this.c.enable;
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Constructor
         */
        _constructor: function () {
            var that = this;
            var dt = this.s.dt;
            var hostSettings = dt.settings()[0];

            dt.on('draw.dtrg', function (e, s) {
                if (that.c.enable && hostSettings === s) {
                    that._draw();
                }
            });

            dt.on('column-visibility.dt.dtrg responsive-resize.dt.dtrg', function () {
                that._adjustColspan();
            });

            dt.on('destroy', function () {
                dt.off('.dtrg');
            });
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Private methods
         */

        /**
         * Adjust column span when column visibility changes
         * @private
         */
        _adjustColspan: function () {
            $('tr.' + this.c.className, this.s.dt.table().body()).find('td:visible')
                .attr('colspan', this._colspan());
        },

        /**
         * Get the number of columns that a grouping row should span
         * @private
         */
        _colspan: function () {
            return this.s.dt.columns().visible().reduce(function (a, b) {
                return a + b;
            }, 0);
        },


        /**
         * Update function that is called whenever we need to draw the grouping rows.
         * This is basically a bootstrap for the self iterative _group and _groupDisplay
         * methods
         * @private
         */
        _draw: function () {
            var dt = this.s.dt;
            var groupedRows = this._group(0, dt.rows({page: 'current'}).indexes());

            this._groupDisplay(0, groupedRows);
        },

        /**
         * Get the grouping information from a data set (index) of rows
         * @param {number} level Nesting level
         * @param {DataTables.Api} rows API of the rows to consider for this group
         * @returns {object[]} Nested grouping information - it is structured like this:
         *    {
         *		dataPoint: 'Edinburgh',
         *		rows: [ 1,2,3,4,5,6,7 ],
         *		children: [ {
         *			dataPoint: 'developer'
         *			rows: [ 1, 2, 3 ]
         *		},
         *		{
         *			dataPoint: 'support',
         *			rows: [ 4, 5, 6, 7 ]
         *		} ]
         *	}
         * @private
         */
        _group: function (level, rows) {
            var fns = $.isArray(this.c.dataSrc) ? this.c.dataSrc : [this.c.dataSrc];
            var fn = DataTable.ext.oApi._fnGetObjectDataFn(fns[level]);
            var dt = this.s.dt;
            var group, last;
            var data = [];
            var that = this;

            for (var i = 0, ien = rows.length; i < ien; i++) {
                var rowIndex = rows[i];
                var rowData = dt.row(rowIndex).data();
                var group = fn(rowData);

                if (group === null || group === undefined) {
                    group = that.c.emptyDataGroup;
                }

                if (last === undefined || group !== last) {
                    data.push({
                        dataPoint: group,
                        rows: []
                    });

                    last = group;
                }

                data[data.length - 1].rows.push(rowIndex);
            }

            if (fns[level + 1] !== undefined) {
                for (var i = 0, ien = data.length; i < ien; i++) {
                    data[i].children = this._group(level + 1, data[i].rows);
                }
            }

            return data;
        },

        /**
         * Row group display - insert the rows into the document
         * @param {number} level Nesting level
         * @param {object[]} groups Takes the nested array from `_group`
         * @private
         */
        _groupDisplay: function (level, groups) {
            var dt = this.s.dt;
            var display;

            for (var i = 0, ien = groups.length; i < ien; i++) {
                var group = groups[i];
                var groupName = group.dataPoint;
                var row;
                var rows = group.rows;

                if (this.c.startRender) {
                    display = this.c.startRender.call(this, dt.rows(rows), groupName, level);
                    row = this._rowWrap(display, this.c.startClassName, level);

                    if (row) {
                        row.insertBefore(dt.row(rows[0]).node());
                    }
                }

                if (this.c.endRender) {
                    display = this.c.endRender.call(this, dt.rows(rows), groupName, level);
                    row = this._rowWrap(display, this.c.endClassName, level);

                    if (row) {
                        row.insertAfter(dt.row(rows[rows.length - 1]).node());
                    }
                }

                if (group.children) {
                    this._groupDisplay(level + 1, group.children);
                }
            }
        },

        /**
         * Take a rendered value from an end user and make it suitable for display
         * as a row, by wrapping it in a row, or detecting that it is a row.
         * @param {node|jQuery|string} display Display value
         * @param {string} className Class to add to the row
         * @param {array} group
         * @param {number} group level
         * @private
         */
        _rowWrap: function (display, className, level) {
            var row;

            if (display === null || display === '') {
                display = this.c.emptyDataGroup;
            }

            if (display === undefined || display === null) {
                return null;
            }

            if (typeof display === 'object' && display.nodeName && display.nodeName.toLowerCase() === 'tr') {
                row = $(display);
            } else if (display instanceof $ && display.length && display[0].nodeName.toLowerCase() === 'tr') {
                row = display;
            } else {
                row = $('<tr/>')
                    .append(
                        $('<td/>')
                            .attr('colspan', this._colspan())
                            .append(display)
                    );
            }

            return row
                .addClass(this.c.className)
                .addClass(className)
                .addClass('dtrg-level-' + level);
        }
    });


    /**
     * RowGroup default settings for initialisation
     *
     * @namespace
     * @name RowGroup.defaults
     * @static
     */
    RowGroup.defaults = {
        /**
         * Class to apply to grouping rows - applied to both the start and
         * end grouping rows.
         * @type string
         */
        className: 'dtrg-group',

        /**
         * Data property from which to read the grouping information
         * @type string|integer|array
         */
        dataSrc: 0,

        /**
         * Text to show if no data is found for a group
         * @type string
         */
        emptyDataGroup: 'No group',

        /**
         * Initial enablement state
         * @boolean
         */
        enable: true,

        /**
         * Class name to give to the end grouping row
         * @type string
         */
        endClassName: 'dtrg-end',

        /**
         * End grouping label function
         * @function
         */
        endRender: null,

        /**
         * Class name to give to the start grouping row
         * @type string
         */
        startClassName: 'dtrg-start',

        /**
         * Start grouping label function
         * @function
         */
        startRender: function (rows, group) {
            return group;
        }
    };


    RowGroup.version = "1.1.2";


    $.fn.dataTable.RowGroup = RowGroup;
    $.fn.DataTable.RowGroup = RowGroup;


    DataTable.Api.register('rowGroup()', function () {
        return this;
    });

    DataTable.Api.register('rowGroup().disable()', function () {
        return this.iterator('table', function (ctx) {
            if (ctx.rowGroup) {
                ctx.rowGroup.enable(false);
            }
        });
    });

    DataTable.Api.register('rowGroup().enable()', function (opts) {
        return this.iterator('table', function (ctx) {
            if (ctx.rowGroup) {
                ctx.rowGroup.enable(opts === undefined ? true : opts);
            }
        });
    });

    DataTable.Api.register('rowGroup().enabled()', function () {
        var ctx = this.context;

        return ctx.length && ctx[0].rowGroup ?
            ctx[0].rowGroup.enabled() :
            false;
    });

    DataTable.Api.register('rowGroup().dataSrc()', function (val) {
        if (val === undefined) {
            return this.context[0].rowGroup.dataSrc();
        }

        return this.iterator('table', function (ctx) {
            if (ctx.rowGroup) {
                ctx.rowGroup.dataSrc(val);
            }
        });
    });


// Attach a listener to the document which listens for DataTables initialisation
// events so we can automatically initialise
    $(document).on('preInit.dt.dtrg', function (e, settings, json) {
        if (e.namespace !== 'dt') {
            return;
        }

        var init = settings.oInit.rowGroup;
        var defaults = DataTable.defaults.rowGroup;

        if (init || defaults) {
            var opts = $.extend({}, defaults, init);

            if (init !== false) {
                new RowGroup(settings, opts);
            }
        }
    });


    return RowGroup;

}));
